組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

4.4 組込み開発では例外処理を使用すべきか?

C++の例外処理は,処理系によって標準準拠度の差(「4.5.3 例外処理に関する問題点」参照)を除けば,それ自体が移植性を考えるうえで問題になることはあまりありません.しかし,組込み開発という事情を考えると,意図的に“例外を使用しない”状況についても考慮する必要が出てきます.

4.4.1 例外処理を使用しないとは?

組込み開発で例外処理を使用すべきかどうかを検討する前に,まずは“例外処理を使用しない”とは何を意味するのかについて明確にしておきたいと思います.例外処理を使用しないといっても,それには次のようにいくつかのレベルがあります.

  1. 例外的な事象を通知するためのモジュール間のインターフェースとして例外処理を使用しない.
  2. 明示的な送出式(すなわちthrow)を使わない.
  3. システムから例外処理の機能を完全に一掃する.

これだけでは少しわかりにくいので,それぞれについて少し補足してみたいと思います.

1と2はよく似ています.1は複数のモジュールにまたがる例外を使わないということであり,モジュールが内部的に例外処理を使用することを禁止していません.モジュールにまたがって例外が送出される場合には,それは一種のインターフェースであり,モジュールとその利用者の間で合意が取れていなければなりませんが,モジュール内部でしか例外を扱わないのであれば,そうした合意は不要になります.

2は,モジュールの内部であっても,明示的な送出式はいっさい使わないことを意味しています.ただし,これだけでは言語レベルの機能が例外を送出することは防げません.具体的には,new演算子,dynamic_cast演算子,そしてtypeid演算子から例外が送出される可能性があります.また,ライブラリなど,既存あるいは外部から調達したモジュールから例外が送出されることも防げません.監視ブロック(try-Block)によってこれらの例外を捕らえるか,あるいは例外を送出する可能性のあるすべての機能の使用も禁止しないかぎり,例外が送出された場合はプログラムが異常終了するしかなくなります. 3は,あたかも言語仕様に例外処理というものがないかのように扱う方法です.そのためには,明示的な送出式の使用はもちろん,あらゆるライブラリや,言語レベルの機能(new演算子など)を実現するためのランタイムからも例外が送出されないようにする必要があります.この選択を行うときは,Embedded C++のような,はじめから例外処理をサポートしない処理系を使用するか,あるいはコンパイルオプションなどを駆使して,例外処理の機能を処理系から排除する必要があります.3の選択は,処理系を標準規格から逸脱させることを意味しています.

このうち,1に関しては,どちらかというと設計思想に関するものですが,状況によってはこの選択をせざるをえない場合もあります.開発メンバーのC++に対する習熟度が低い場合などは,コーディング規約などで安易に2を選択してしまいがちですが,先ほども書いたように,この方法では決して例外が送出されないわけではありませんので,システムがなぜかときどき落ちるといった不可解な現象を引き起こしがちです.そのため,2の方法はあまり優れているとはいえません.3は,標準規格から逸脱しますし,処理系によってはこのような対応ができない場合もあるわけですが,例外処理を使用しない選択としては最も有効なものです.このように考えると,例外処理を使用しない場合の有効な選択肢は,1または3ということになりそうです.

4.4.2 例外処理を使用できない, または使用したくない状況

次に,例外処理を使用できない,または使用したくない状況について考えてみることにします.例外処理を使用できない状況には次のようなものがあります.

  • (A) 処理系が例外処理をサポートしていない.
  • (B) システム全体を分割リンクする(複数のロードモジュールを使用する).
  • (C) CをはじめとしたC++以外の言語で記述されたモジュールが混在する.

このうち,(A)に関しては例外処理を使用できないことは明らかですので,先に例外処理を使用しない場合のレベルとして挙げた3を選択する必要があります.処理系が例外処理をサポートしない場合というのは,Embedded C++のような非標準処理系だけではなく,例外処理自体はサポートしているものの,マルチタスク環境など,それが正常に動作しないような場合も含みます.

(B)は,何らかの理由でロードモジュールを分割するような場合です.例外処理はその仕組み上,どのような経路で関数が呼び出され,途中にどんなデストラクタがあったかを記録するので,分割リンクされると正しく機能しなくなる場合がほとんどです.このような状況では,それぞれのロードモジュールの内部では例外処理を使えますが,ロードモジュール間のインターフェースとして例外を使用することはできなくなります.結果として,先に挙げた1を選択する必要があります.

(C)は,ほとんど場合,CとC++のコードが混在する状況です.たとえば,C++→C→C++の順に呼び出される場合,真ん中のCで記述された関数は,C++の例外を上記の階層に正しく伝播できるかどうかわかりません(処理系によっては可能ですが……).このような状況では,やはり,先に挙げた1を選択する必要があります.また,Cなどの他言語から呼び出される関数内では,例外を送出するようなコードは避けたほうが無難です.コールバック関数を使う場合にこうした状況になることが多く,問題点にも気づきにくいので特に注意が必要です.

それでは,今度は例外処理を使用したくない状況についてです.

  • (D) 例外的な事象を高速に処理したい.
  • (E) プログラムサイズをできるかぎり小さくしたい.
  • (F) 開発メンバーのC++に対する習熟度が低い.

(D)に関しては,システム全体で例外処理を使うかどうかではなく,高速に処理したい事象についてだけ,選択的に別の方法(たとえば,返却値としてエラーコードを返す方法)にするだけでかまいません.比較的発生頻度の高い事象や,リアルタイム性が要求される状況では,例外を用いて処理すべきではありません.

(E)に関しては,try,catch,throwというキーワードを明示的に使用しないだけではプログラムサイズを小さくすることはできません.処理系にかなり依存しますが,明示的なデストラクタ定義の回避や,インライン関数の活用など,かなり技巧的な対策が必要になります.最も確実な方法は,先に挙げた3の方法のように,システム全体から例外処理の機能を排除してしまうことです.

(F)はなかなかやっかいですが,習熟度が非常に低い場合は,先に挙げた3の方法で,システム全体から例外処理の機能を排除するしかないと思います.基本的に,C++はCの延長線上にあるプログラミング言語である以上,プログラマーが十分なスキルを持っていることを前提としており,プログラマーを信用するというスタンスで設計されています.Cでも,習熟度が低い技術者はポインタの扱いを誤って問題を引き起こすことがよくありますが,それを理由にポインタの使用を禁止するのはやりすぎです.例外処理についても同じことがいえるのではないかと思います.

以上で例外処理に関する話題をいったん終えます.例外処理は,使いこなせれば非常に便利な機能ですが,使いこなせなければ,それに伴うオーバーヘッドや危険性を考えると割に合いません.特に,C++の習熟度が低いメンバーを中心に開発を行うような状況では,可能なかぎり,例外処理をシステム全体から排除したほうが無難なことは確かです.しかし,例外処理を排除した状態で設計およびコーディングされたソースコードは,後から例外処理に対応させることは非常に困難です.目先の無難さをとるのか,将来的な再利用性をとるのか,どちらを選択するかは,それぞれの開発現場の状況に応じて決定するしかなさそうです.